관통 질문: 이 읽기/쓰기가 버퍼의 경계(시작·끝·인덱스 범위) 안에 있다고 누가 보장하는가?
C는 배열이나 포인터로 버퍼 밖을 읽고 써도 언어가 막아주지 않는다. 오류가 나는 게 아니라 “옆에 있는 메모리”가 조용히 바뀐다. 6개 항목은 위(끝을 넘는다)·아래(시작보다 앞선다)·옆(널 종료 없이 계속 간다)으로 방향만 다를 뿐 원인은 하나다 — “이 복사·접근이 버퍼 크기를 넘지 않는지 아무도 확인하지 않았다.”
11. 스택에 할당된 버퍼 오버플로우 CWE-121
신뢰 경계: 함수 인자 문자열 → 경계 검사 없이 → 스택 지역 버퍼로 복사
⚠️ strcpy/strcat/sprintf/gets는 보이면 즉시 교체 대상. strncpy를 써도 마지막 바이트 널 종료는 직접 채워야 한다.
공격 시나리오: 24바이트 지역버퍼에 더 긴 문자열을 strcpy() 로 복사하면 반환주소까지 덮여, 공격자가 실행 흐름을 자신의 코드로 돌릴 수 있다.
❌ Bad
void manipulate_string(char* string){ char buf[24]; strcpy(buf, string); /* 길이 검사 없는 복사 */}
✅ Good
void manipulate_string(char* string){ char buf[24]; if (strlen(string) < sizeof(buf)) strncpy(buf, string, sizeof(buf)-1); buf[sizeof(buf)-1] = '\0'; /* 반드시 null 로 종료 */}
치환 규칙: strcpy → 길이 비교 후 strncpy + 마지막 바이트 수동 널 종료.
보충 — 길이 검사보다 먼저: 빈 값(NULL) 검사
위 Good 예시는 strlen(string) 을 바로 호출하는데, string 이 NULL 로 들어오면 strlen(NULL) 자체가 정의되지 않은 동작(크래시)이다. 길이 비교로 넘어가기 전에 포인터가 유효한지부터 확인해야 한다.
void manipulate_string(char* string){ char buf[24]; if (string == NULL) return; /* 빈 값 방어가 먼저 */ if (strlen(string) < sizeof(buf)) strncpy(buf, string, sizeof(buf)-1); buf[sizeof(buf)-1] = '\0';}
컴파일러 차원의 방어(코드 수정을 대체하지 않는 보완): gcc는 스택 오버플로우를 탐지해 실행을 즉시 중단시키는 옵션을 제공한다.
-fstack-protector-all : 모든 함수에 스택 카나리를 심어, 반환 직전 카나리 값이 바뀌었으면 __stack_chk_fail 로 즉시 abort.
-D_FORTIFY_SOURCE=2(-O1 이상과 함께) : strcpy/sprintf 등을 크기 인지 버전으로 치환해, 알려진 버퍼 크기를 넘는 호출을 컴파일/런타임에 감지.
-fsanitize=address(ASan, 개발·테스트 빌드용) : 경계를 넘는 순간 정확한 위치를 리포트하며 중단.
단, 이 옵션들은 탐지 후 즉시 죽이는 것이지 취약점 자체를 없애는 것이 아니다 — 서비스가 죽는 DoS는 여전히 성립하므로 근본 대책(길이·NULL 검사)을 대체할 수 없다.
12. 힙에 할당된 버퍼 오버플로우 CWE-122
신뢰 경계: 외부 토큰 개수 → 경계 검사 없이 → malloc() 버퍼 인덱스
⚠️ 힙이든 스택이든 “할당 크기”와 “쓰기 인덱스”는 한 쌍으로 관리하고, 복사 함수는 크기 인자를 받는 것으로 통일한다.
공격 시나리오: 토큰 개수가 할당 크기를 넘으면 힙 관리 구조체가 덮여, free() 시점에 임의 주소 쓰기로 이어져 코드 실행이나 프로세스 다운을 유발한다.
flowchart LR
A["1단계: 쓰기 능력 획득<br/>인접 청크의 size·fd·bk<br/>또는 옆 구조체를 덮어씀"] --> B["2단계: 실행 흐름 탈취<br/>함수 포인터·GOT·반환주소가<br/>덮인 값으로 호출/점프됨"]
B --> C["공격자가 지정한 주소로 이동<br/>(NX 때문에 기존 코드 조각을<br/>이어 붙이거나 system() 등으로 점프)"]
순번 12의 dataBuffer[dataIndex] 는 인덱스와 값을 모두 외부가 정하므로, 오버플로우를 거치지 않고 처음부터 임의 주소 쓰기가 된다.
safe unlinking · NX/DEP · ASLR · 스택 카나리 같은 완화 기술은 비용을 올릴 뿐, 정보 유출 취약점 하나와 결합하면 우회된다. 권한 플래그 변조나 DoS는 완화 기술과 무관하게 그대로 성립한다.
결론: 오버플로우는 “옆 메모리를 쓸 수 있는 능력”을 줄 뿐이다. 애초에 경계를 넘기지 않는 것이 유일한 근본 대책이다.
13. 버퍼 시작 지점 이전에 쓰기 CWE-124
신뢰 경계: 토큰 개수(외부 입력) → 하한 검사 없이 → 감소하는 배열 인덱스
⚠️ 오버플로우는 위(상한)뿐 아니라 아래(하한)로도 발생한다. 감소 인덱스와 포인터 뺄셈에도 하한 검사를 붙여라.
공격 시나리오: 토큰 수가 배열 크기보다 많으면 인덱스가 0 아래로 내려가, 버퍼 앞쪽의 다른 변수나 스택 프레임이 덮인다.
❌ Bad
int wordIndex = MAX_WORD_COUNT - 1;while (token != NULL) { /* 하한 검사 없이 감소 */ words[wordIndex] = token; token = strtok(NULL, " \t"); wordIndex--; }
✅ Good
vector<char *> words(MAX_WORD_COUNT, NULL);try { while (token != NULL) { words.at(wordIndex) = token; /* at() 이 범위 검사 */ token = strtok(NULL, " \t"); wordIndex--; } }catch (const std::exception & e) { /* out_of_range 처리 */ }
치환 규칙: 감소 루프·포인터 뺄셈에 하한(0) 검사 명시, 가능하면 범위 검사가 내장된 접근자(.at()) 사용.
14. 범위 초과해서 읽기 CWE-125
신뢰 경계: 배열 인덱스 → 상한 검사 없이 → 배열 참조
⚠️ 배열 포인터를 넘길 때는 반드시 길이도 함께 넘긴다. “포인터 단독 전달”이 out-of-bounds read의 근본 원인이다.
공격 시나리오: 배열 뒤에 있던 세션 토큰·암호키가 반환값에 실려 그대로 흘러나가고, 값에 포인터가 섞이면 실제 주소가 드러나 ASLR이 무력화된다. 이 주소는 11·12번 버퍼 오버플로우를 정확히 겨냥하는 열쇠가 된다.
❌ Bad
int readNextData(int * data_pool) { static int curIdx = 0; int END_MARK = -1; if (data_pool[curIdx] != END_MARK) /* 마커 없으면 폭주 */ { curIdx++; return data_pool[curIdx]; } return END_MARK; }
✅ Good
int readNextData(int * data_pool, int size) { static int curIdx = 0; int END_MARK = -1; if (curIdx >= size) return END_MARK; /* 크기 검사 먼저 */ if (data_pool[curIdx] != END_MARK) { curIdx++; return data_pool[curIdx]; } return END_MARK; }
치환 규칙: 배열을 넘길 때 크기 인자를 항상 동반, 종료 마커만 믿지 말고 길이 기반 종료 조건을 함께 둔다.
주의 — 크기 대신 \0 을 믿는 순간 생기는 역설
“크기를 항상 함께 넘긴다”가 원칙이지만, 실무 코드에서는 문자열을 다룰 때 크기 인자 없이 \0 을 만날 때까지를 종료 조건으로 쓰는 경우가 흔하다. 이때 함정은: 그 문자열에 \0 이 정상적으로 박혀 있다는 보장이 없으면, “크기 검사를 \0 검사로 대체”한 것 자체가 새로운 범위 초과 읽기(over-read)를 만든다.
버퍼를 채울 때 널 종료를 실수로 빼먹으면(예: read()/memcpy() 로 정확히 버퍼 크기만큼만 채운 경우), 그 뒤로 이어지는 문자열 함수는 우연히 다른 메모리에서 \0 을 만날 때까지 계속 읽는다 — 16번 널 종료 문제(CWE-170) 와 원인이 같다.
즉 “크기 인자 없이 \0 로 대체”는 CWE-170(널 종료 누락)이 발생하는 순간 그대로 CWE-125(범위 초과 읽기)로 전이된다. 둘은 서로 다른 취약점이 아니라 같은 실수의 앞뒤 단계다.
방어는 둘 중 하나가 아니라 둘 다: (1) 문자열을 만들 때 널 종료를 명시적으로 보장(strlcpy/직접 대입), (2) 그 문자열을 읽을 때도 가능하면 크기 상한을 별도로 둔다(strnlen 처럼 최대 길이를 받는 함수 사용).
15. 검사되지 않은 배열 인덱싱 CWE-129
신뢰 경계: 외부 정수(sscanf 결과) → 하한·상한 검사 없이 → 배열 인덱스
⚠️ 외부에서 온 정수는 그 자체로 “인덱스 후보”일 뿐이다. 하한과 상한을 모두 검증하고 통과한 값만 [] 안에 넣어라.
공격 시나리오: 배열 접근은 base + index × 원소크기 이므로 인덱스를 조작하면 원하는 거리로 한 번에 점프해 중간 보호 장치를 건너뛴다.
❌ Bad
if (sscanf(buf, "%d %d", &num, &size) == 2) sizes[num-1] = size; /* 최대·최소 검사 없음 */